/**
 * 
 */
package com.novartis.nims.imagedc.logic;

import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;
import org.apache.oro.text.regex.MalformedPatternException;
import org.rsna.ctp.stdstages.anonymizer.AnonymizerStatus;

import com.novartis.nims.imagedc.anonymizer.DicomCleaner;
import com.novartis.nims.imagedc.dataaccess.ScanServiceDao;
import com.novartis.nims.imagedc.util.Filesystem;
import com.novartis.nims.imagedc.util.RecursiveFileListIterator;
import com.pixelmed.dicom.DicomException;

/**
 * @author snydejo2
 *
 */
public class ScanManager {
	
	private static Logger logger = Logger.getLogger(ScanManager.class);
	
	public static final String PROGRESS_PROPERTY = "progress";
	public static final String MESSAGE_PROPERTY = "message";
	
	private int currentProgress = 0;
	private String currentMessage = "";
	
	private Map<String, Scan> scans = new HashMap<String, Scan>();
	
	private Map<String, Boolean> scanSelections = new HashMap<String, Boolean>();
	
	public Collection<Scan> getScans() {
		return scans.values();
	}
	
	public Collection<Boolean> getScanSelections() {
		return scanSelections.values();
	}

	private PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
	private WorkflowController workflowController = null;
	
	private AttributeTagDelegate scanKeyTag = new AttributeTagDelegate("0020", "000E");
	
	private RuleSet rules = null;
	public void setRules(RuleSet values) {
		rules = values;
	}
	
	private Properties deidentificationProperties = null;
	public void setDeidentificationProperties(Properties value) {
		deidentificationProperties = value;
	}
	
	private String submissionUrl = null;
	public void setSubmissionUrl(String value) {
		submissionUrl = value;
	}

	/**
	 * 
	 */
	public ScanManager() { }
	
	private final void setProgress(int progress) {
		if (! propertyChangeSupport.hasListeners(PROGRESS_PROPERTY)) {
            return;
        }
        int oldValue = currentProgress;
        int newValue = currentProgress = progress;
        propertyChangeSupport.firePropertyChange(PROGRESS_PROPERTY, oldValue, newValue);
	}
	
	private final void setMessage(String message) {
        if (! propertyChangeSupport.hasListeners(MESSAGE_PROPERTY)) {
            return;
        }
        String oldValue = currentMessage;
        String newValue = currentMessage = message;
        propertyChangeSupport.firePropertyChange(MESSAGE_PROPERTY, oldValue, newValue);
	}
	
	private final boolean isCancelled() {
		if (workflowController == null) {
			return false;
		} else {
			return workflowController.isCancelled();
		}
	}
	
	public void registerWorkflowController(WorkflowController controller) {
		if (workflowController != null) {
			propertyChangeSupport.removePropertyChangeListener(workflowController);
		}
		workflowController = controller;
		propertyChangeSupport.addPropertyChangeListener(workflowController);
	}
	
	public void loadScansFromDirectory(File directory) throws Exception {
		logger.debug("loadScansFromDirectory called. directory = " + directory.getAbsolutePath());
		
		long totalBytes = Filesystem.sizeInBytes(directory);
		if (totalBytes < 1) {
			//TODO: replace with custom exception type
			throw new Exception("Couldn't find any files in source directory. " +
					"Try checking the path to the subject directories, or the contents of the " +
					"subject directories.");
		}
		
		setProgress(0);
		//setDeterminate(true);
		setMessage("Grouping image files into scans...");

		long currentBytes = 0;
		RecursiveFileListIterator fileIterator = new RecursiveFileListIterator(directory);
		while (fileIterator.hasNext() && (! isCancelled())) {
			File file = fileIterator.next();
			
			logger.debug("associating file to scan: " + file.getAbsolutePath());
			
			addFileToScan(file);
			
			currentBytes += file.length();
			setProgress((int) Math.min(100, 100 * currentBytes / totalBytes));
		}
	}
	
	private void addFileToScan(File file) throws Exception {
		String scanKey = null;
		ImageResource image = new ImageResource(file.toURI());
		try {
			scanKey = image.getAttributeValue(scanKeyTag, false);
		} catch (DicomException e) {
			// TODO retry with ignore pixel data = true?
			throw e;
		}
		
		if (scanKey == null || scanKey.equals("")) {
			throw new Exception("ScanKey tag " + scanKeyTag.toString() + " is missing or empty in file " + file.getAbsolutePath());
		} else {
			Scan scan = scans.get(scanKey);
			
			if (scan == null) {
				scan = new Scan();
				scan.setScanKey(scanKey);
				scans.put(scanKey, scan);
				scanSelections.put(scanKey, Boolean.TRUE);
			}
			
			scan.addImage(new ImageResource(file.toURI()));
			logger.debug("File added to scan (scanKey: " + scanKey + "; uri: " + file.toURI() + ")");
		}
	}
	
	public void calculateScanIdentifiers() throws MalformedPatternException {
		setMessage("Determining scan identifiers...");
		
		Map<String, Integer> scanNameRepeatMap = new HashMap<String, Integer>();
		int index = 0;
		setProgress(index);
		for (Scan scan : scans.values()) {
			if (isCancelled()) {
				return;
			}
			
			String scanName = rules.getMatchingScanName(scan);
			
			Integer repeatNumber = scanNameRepeatMap.get(scanName);
			if (repeatNumber == null) {
				repeatNumber = 1;
			} else {
				repeatNumber++;
			}
			scanNameRepeatMap.put(scanName, repeatNumber);

			scan.setScanName(scanName);
			scan.setScanRepeatNumber(repeatNumber.toString());
			logger.debug("Scan identified (scanName: " + scanName + "; rn: " + repeatNumber.toString() + ")");
			
			index++;
			setProgress((int) Math.min(100, 100 * index / scans.values().size()));
		}
	}

	public void updateAndSubmitScans(boolean anonymizeFlag) throws Exception {
		setMessage("Anonymizing images and copying to trial submission directory...");
		logger.info("Started deidentifying scans.");

		if (deidentificationProperties == null) {
			logger.warn("Couldn't find any deidentification rules. Skipping all scans.");
		} else {
			int scanSize = scans.values().size();
			int scanCount = 0;
			setProgress(scanCount);
			File tempDir = File.createTempFile("ImagEDC-", "_tmp");
			tempDir.delete();
			tempDir.mkdir();
			tempDir.deleteOnExit();
			for (String scanKey : scans.keySet()) {
				if (scanSelections.get(scanKey).equals(Boolean.TRUE)) {
					Scan scan = scans.get(scanKey);
					
					if (isCancelled()) {
						logger.info("Scan deidentification and submission canceled.");
						return;
					}
					
					Set<ImageResource> deidentifiedImages = new HashSet<ImageResource>();
					
					Iterator<ImageResource> imageIterator = scan.iterator();
					while (imageIterator.hasNext()) {
						File inFile = new File(imageIterator.next().getUri());
						logger.info("Deidentifying file " + inFile.getAbsolutePath());
						
						File outFile = File.createTempFile("anon_", ".dcm", tempDir);
						outFile.deleteOnExit();
						
						deidentifyFile(inFile, outFile, deidentificationProperties);
						
						imageIterator.remove(); //remove the original image from the scan
						deidentifiedImages.add(new ImageResource(outFile.toURI())); //keep track of the deidentified image
					}
					
					//add the deidentified images to the scan
					scan.addAllImageFiles(deidentifiedImages);
					scan.setBasePath(tempDir.toURI());
					
					scanCount++;
					setProgress((int) Math.min(100, 100 * scanCount / scanSize));
				}
			}

			scanCount = 0;
			ScanServiceDao dao = new ScanServiceDao(submissionUrl);
			setMessage("Submitting scans to NIMS Scan Service...");
			logger.info("Started submitting scans.");
			for (String scanKey : scans.keySet()) {
				if (scanSelections.get(scanKey).equals(Boolean.TRUE)) {
					Scan scan = scans.get(scanKey);
					if (isCancelled()) {
						logger.info("Scan deidentification and submission canceled.");
						return;
					}
					dao.submitScan(scan);
					scanCount++;
					setProgress((int) Math.min(100, 100 * scanCount / scanSize));
				}
			}
		}
	}

	private void deidentifyFile(File inFile, File outFile, Properties properties) throws Exception {
		// perform the anonymization
		AnonymizerStatus result = DicomCleaner.anonymize(
				inFile, outFile,
				properties, null,
				false, false, false);
		
		if (!result.isOK()) {
			//TODO: define custom exception type
			throw new Exception(inFile.getAbsolutePath() + " could not be anonymized.");
		}
	}

	public void setScanStudyIds(String studyId) {
		for (Scan scan : scans.values()) {
			scan.setStudyId(studyId);
		}
	}
	
	public void clearScans() {
		scans.clear();
	}
}